import datetime
import os
import time
import logging
from typing import Union, Dict, List
import errno
import threading
import socket
from concurrent.futures import TimeoutError

from concurrent_log_handler import ConcurrentRotatingFileHandler
from opcua import ua
from opcua.ua import UaStatusCodeError, AccessLevel
from opcua.common.node import Node
from opcua.client.client import Client

LOGGER = {
    "LOGGER_NAME": "data_pedestal",
    "TOTAL_LEVEL": logging.INFO,
    "FILE_HANDLER_LEVEL": logging.DEBUG,
    "CONTROL_HANDLER_LEVEL": logging.INFO,
    "FILE_NAME": r"log.txt",
    "MODE": "a",
    "MAX_BYTES": 10 * 1024 * 1024,
    "BACKUP_COUNT": 10,
    "ENCODING": "UTF-8",
    "FORMAT": "%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s"
}

pid = os.getpid()


def get_logger() -> logging.getLogger():
    logger = logging.getLogger(LOGGER['LOGGER_NAME'])
    logger.setLevel(LOGGER['TOTAL_LEVEL'])  # 设置日志级别
    file_handler = ConcurrentRotatingFileHandler(filename=LOGGER['FILE_NAME'], mode=LOGGER['MODE'],
                                                 maxBytes=LOGGER['MAX_BYTES'],
                                                 backupCount=LOGGER['BACKUP_COUNT'],
                                                 encoding=LOGGER['ENCODING'])  # 设置输出文件
    file_handler.setLevel(LOGGER['FILE_HANDLER_LEVEL'])
    control_handler = logging.StreamHandler()
    control_handler.setLevel(LOGGER['CONTROL_HANDLER_LEVEL'])
    formatter = logging.Formatter(LOGGER['FORMAT'])
    file_handler.setFormatter(formatter)
    control_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.addHandler(control_handler)
    return logger


# 创建一个实例
logger = get_logger()


class OpcuaClient:

    def __init__(self, url):
        self.url = url
        self._client = Client(url=url)
        self.eps = {}
        self.root_node = None
        self.objects_node = None
        self.node_set = set()
        self.connected = False
        self._lock = threading.Lock()
        self.connect()  # 疑问 是否影响遍历地址空间
        self.address_space = []

    def node_name(self, node: Node):
        """
        获取node的名字，比如node
        :param node:
        :return:
        """
        return node.get_browse_name().Name

    def node_class_name(self, node: Node, use_name=True):
        """
        获取节点的class name
        name:Object = value:1
        Variable = 2
        Method = 4
        :param node:
        :return:
        """
        if use_name:
            return node.get_node_class().name
        return node.get_node_class().value

    def get_node_info(self, node: Node):
        """
        获取node信息
        :param node: node对象
        :return:
        """
        node_item = {
            'node_name': '',
            'node_id': '',
            'node_class': '',
            'childrens': [],
            'parentId': None,
            'message_info': '',
            'err_info': None
        }
        try:
            # 获取节点名称
            node_name = self.node_name(node)
            node_item['node_name'] = node_name
            # node id 字符串
            node_str = node.nodeid.to_string()
            node_item['node_id'] = node_str
            # 获取节点类型
            node_class_name = self.node_class_name(node)
        except Exception as e:
            err_info = str(e)
            node_item['err_info'] = err_info
            return node_item
        else:
            if node_class_name == 'Method':
                return self.get_method_node_info(node_item)
            elif node_class_name == 'Variable':
                return self.get_variable_node_info(node, node_item)
            elif node_class_name == 'Object':
                return self.get_object_node_info(node_item)

    def get_variable_node_info(self, node: Node, node_item):

        try:
            node_value_obj = node.get_data_value()
            node_value = node_value_obj.Value.Value
            node_value = str(node_value)
            node_value_type = node_value_obj.Value.VariantType.name
            node_value_status = node_value_obj.StatusCode.name
            node_value_timestamp = node_value_obj.SourceTimestamp
            node_access_level = node.get_access_level()  # CurrentRead:1, CurrentRead  CurrentWrite:3
            node_item[
                'message_info'] = """--|--Node class Variable--|--node_name {}--|--node_id {}--|--Access Level {}--|--node_value_type {}--|--node_value {}--|--node_status {}--|--""".format(
                node_item["node_name"],
                node_item["node_id"],
                str(node_access_level),
                str(node_value_type),
                node_value,
                str(node_value_status)
            )
        except Exception as e:
            print(e)
            node_item['err_info'] = str(e)
            return node_item
        else:
            return node_item

    def get_method_node_info(self, node_item: Dict):
        node_item['node_class'] = 'Method'
        node_item[
            'message_info'] = f'--|--Node class Method--|--node_name {node_item["node_name"]}--|--node_id {node_item["node_id"]}--|--'
        return node_item

    def get_object_node_info(self, node_item: Dict):
        node_item['node_class'] = 'Object'
        node_item[
            'message_info'] = f'--|--Node class Object--|--node_name {node_item["node_name"]}--|--node_id {node_item["node_id"]}--|--children nums {len(node_item["childrens"])}--|--'
        return node_item

    def disconnect(self):
        """
        断开连接
        """
        if self.connected:
            self._client.disconnect()

    def connect(self):
        if self.connected:
            return
        # 由于不能重复 connect, 故需先断开一次
        try:
            self._client.disconnect()
        except (AttributeError, OSError, TimeoutError):
            pass
        while True:
            try:
                self._client.connect()
                self.connected = True
                return
            except (ConnectionRefusedError, socket.timeout):
                logger.error(
                    "和 OPCUA 设备之间的连接建立失败, 请检测设备是否正常, 以及当前机器和 PLC 设备之间的网络是否畅通")
            except OSError as e:
                if e.errno == errno.EHOSTUNREACH:
                    logger.error("当前主机似乎处于一个没有网络的环境中")
                else:
                    logger.error("当前主机的网络似乎存在问题")
            time.sleep(5)

    def browse_eps(self):
        """
        获取节点
        :return:example:
        {'application_url_list': ['urn:SIMATIC.S7-1500.OPC-UAServer:PLCSim Advanced'],
         'discovery_url_list': ['opc.tcp://192.168.181.181:4840']}
        """
        application_url_list = []
        discovery_url_list = []
        for item in self._client.find_servers():
            application_url_list.append(item.ApplicationUri)
            discovery_url_list = [_item for _item in item.DiscoveryUrls]
        self.eps = {'application_url_list': application_url_list, 'discovery_url_list': discovery_url_list}

    def get_root_node(self):
        """
        获取到node节点
        :return:
        """
        self.root_node = self._client.get_root_node()

    def get_objects_node(self):
        """
        获取到objects节点
        :return:
        """
        self.objects_node = self._client.get_objects_node()

    def read_data_once(self, node_id):
        """
        输入node_id 返回测试值
        :param node_id:
        :return:
        """
        return self._client.get_node(node_id).get_value()

    def get_dict_from_list(self, dataset: List, node_name: str, by_key: str):
        """
        从一个列表中按照key获取一个字典项
        :param dataset: [{},{},{}]
        :param node_name: 节点名称
        :param by_key:按照字典中的哪个key查找
        :return:
        """
        for item in dataset:
            if item[by_key] == node_name:
                return item
            else:
                continue

    def browse(self, root: dict, level: int, display_name: str):
        """
        遍历opcua地址空间
        :param dataset:[{},{},{}]
        :param root:根节点{}
        :param level:层级
        :param display_name: 遍历时候显示的名称
        :return:
        """
        # print(f'{level * "-"}{root["node"]}')
        print(f'{level * "-"}{root[display_name]}')
        level += 1
        for item in root['childrens']:
            self.browse(self.get_dict_from_list(self.address_space, item, "node_name"), level, "node_name")

    def read_node(self, node: str):
        node_obj = self._client.get_node(node)
        if AccessLevel.CurrentRead in node_obj.get_access_level():
            res = node_obj.get_value()
            print(res)


    def write_node(self):
        pass

    def read_nodes(self, node_list: list[str]):
        for _node in node_list:
            value = self.read_node(_node)


if __name__ == '__main__':
    opc = OpcuaClient(url='opc.tcp://49.232.190.174:4840/')
    opc.read_node("ns=2;i=2")
    opc.read_node("ns=2;i=1")
    opc.read_node("i=85")
    opc.read_node("i=85")
    opc.read_node("i=84")
    opc.read_node("i=2994")
    opc.disconnect()